-
Notifications
You must be signed in to change notification settings - Fork 54
SGP30 / SGP40 background sampling #811
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add SGP30 / SGP40 background sampling. Requires frequently 1s polling or returns 0 values.
0ee75ca
to
662f61c
Compare
also added sgp30 averaging option
I can confirm the SGP40 works with this PR. I tested using: Adafruit Qt Py ESP32-S3 (2MB PSRAM) Trying different settings from 1 second --> 15 minute update intervals for prolonged periods (hours) was working with the keep alive code. The SGP30 changes are untested as I don't have one on hand. |
brent and tyeth are both out right now but they'll review when return! |
Thanks @mikeysklar, this has been weighing on my mind for a while as there are a few we currently "support" that are out of spec in their current use. I thought there was an existing issue, but can't find one, must be thinking of forum discussions. The only other previous related thoughts were that it might be worth having an internal poll period defined for each component (with a matching entry in the components repo JSON definition), and then a separate publish period (the current thing in definition file), both potentially displayed to the user (publish >= poll in interface) although we'd then need to specify max and min as well as default internal poll period (fastTick speed). On the averaging front, it might be best to leave it to the current techniques, as I think it's probably expected that the moment it publishes data the value represents the current value and not a time accumulated average. The user can see averaged data instead using the dashboard and feed pages and APIs (charts and API get access to aggregated data at various time intervals), and possibly Actions. These are all minor things, and more an attempt to uncork my stored up thoughts on the issue. I'll bring it up with Brent as to how he'd like to proceed, which will be next week, but I can't see any problem with the current idea at all. |
Thank you for the larger explanation. Using a polling frequency value in the JSON config makes a lot of sense. Another issue @Timeline pointed out is deep sleep mode which I believe is still in the works. I bring it up here only that continuous background polling could cancel out the ability to use sleep mode with these sensors. A few more options to consider as JSON config values:
If you are doing hourly air quality samples it doesn't make a lot of sense to keep the controller awake and polling every second. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this makes sense to me as an implementation for a few drivers in WipperSnapper beta.
We're currently implementing the new WipperSnapper API (v2) and have time to consider alternatives for specific things, like background sampling, into the new API design.
@tyeth If you also approve of this design, I wouldn't be opposed to merging it in. We can talk about it more tomorrow or in this PR.
@mikeysklar I've requested a few design/syntax changes. Please hold off on these until Tyeth also approves.
for (auto *drv : drivers) { | ||
if (drv) | ||
drv->fastTick(); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not use auto
here or create a loop. It is redundant and adds a performance overhead for what should be a tight loop.
Instead, try calling drv->fastTick()
when we are looping through the vectors below. Since fastTick
is virtual
, and the vect. of drivers exists, you dont need to check for nullptr
either.
while (sensorsReturningFalse && retries > 0) { | ||
sensorsReturningFalse = false; | ||
retries--; | ||
curTime = millis(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would curTime
be modified here?
return; | ||
|
||
uint32_t now = millis(); | ||
if (now - _lastFastMs >= 1000) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1000
is a magic number, please #define
what it is at the top of this header.
uint32_t _lastFastMs = 0; | ||
|
||
/** Number of samples accumulated since last publish. */ | ||
uint32_t _n = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_num_samples
_n = 0; | ||
_vocSum = 0.0f; | ||
_rawSum = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move these to the constructor
uint32_t _lastFastMs = 0; | ||
|
||
/** Number of samples accumulated since last publish. */ | ||
uint32_t _n = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change to _sample_num
or something more descriptive
if (!iaqEnabled()) | ||
return; // nothing enabled, save cycles | ||
uint32_t now = millis(); | ||
if (now - _lastFastMs >= 1000) { // ~1 Hz cadence |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1000
is a magic number, please #define it.
Another issue @Timeline pointed out is deep sleep mode which I believe is
still in the works. I bring it up here only that continuous background
polling could cancel out the ability to use sleep mode with these sensors.
That was my nod to Low Power mode, a.k.a. deep sleep support, which will be
v2 of WipperSnapper (both Offline and Online mode - currently only Offline
mode
<https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/tree/offline-mode/src>
uses v2 codebase).
A few more options to consider as JSON config values:
* ramp up time (eg. 30sec - 1min)
* number of initial values to throw away
* deep sleep duration (realistic range)
Yeah I like these. Less so the deep sleep duration, or at least it then
prompted a bit of confusion. I initially imagined deep-sleep-duration as
unnecessary because it's just the normal polling period people select, but
then realise maybe you're referring to the usable range for low power /
sleep usage which gets more complicated/useful.
I wonder if ramp up time is the time until good data, or just any
semi-usable data, and whether users would prefer to still report it
(advanced config option?) during that period. Or is ramp up simply until
any data that shouldnt be 0/NaN/etc, like 15s for SGP30 mentioned later,
not until the internal algorithm is happy (do we need another field related
to stability/quality of data after x amount of time).
If you are doing hourly air quality samples it doesn't make a lot of
sense to keep the controller awake and polling every second.
Agreed, the one issue in the back of my mind is the air quality sensors (or
any type) that have an inbuilt algorithm, rather than raw data (Sensirion
VOC/IAQ index plus some others). They require the baseline
values/coefficients to be trained for ages and regularly stored (ideally
after each reading) and even then are [only valid for the device being
asleep about 10mins max](
https://github.com/adafruit/Adafruit_SGP40/blob/741a139e73e95273d6b5ac074c780aa74597bb9e/src/sensirion_voc_algorithm.h#L133C51-L134C52)
(otherwise [the full baseline compensation run must be performed at next
wake/boot](
https://github.com/adafruit/Adafruit_SGP40/blob/741a139e73e95273d6b5ac074c780aa74597bb9e/src/sensirion_voc_algorithm.h#L122-L123)
).
I guess that 10minute limit would be the "max deep sleep duration",
assuming the "ramp up time" was 3hours and not performed on wake (so not
after a <10min low power sleep), and we probably want to do the fastest
tick possible when throwing away values (rather than waiting for poll
period before broadcasting first value on a fresh boot - maybe not for a
waking boot).
In the back of my mind I'm thinking that throw-away-values might fall
apart quickly if the sensor has a slow internal poll period (calling that
fastTicks in this PR), like the 5seconds minimum poll period on one air one
(I forget which - SCD?). I guess we'd just drop the first value and no
others in that situation (it's real CO2 not algorithm based so 30s ramp up
IIRC).
I'm not sure in the air sensor situation what a user would prefer/expect,
personally I want correct data more than power saving, so ideally I'd see
the first boot not put the sensor to sleep until at least 3hours had
elapsed so the baseline could be saved. I guess it could sleep the mcu
almost immediately as long as the sensor remains powered, and it internally
keeps updating the algorithm without being read (assuming in appropriate
mode) like the SEN4x/5x do (internally updating the algo at 1Hz IIRC).
Forgetting those Sensirion ones for a second, and still looking at VOC,
then we only sell the ENS160 (<3 minutes warm-up <1 hour start-up), and the
BME68x (30mins each startup).
Switching to look at CO2, or better yet eCO2 (estimated/equivalent), then
we only really have the ENS160 + Sensirion SGP and SCD series. Taking the
SGP30 we could benefit from dropping the first 15seconds of data
<https://cdn-learn.adafruit.com/assets/assets/000/050/058/original/Sensirion_Gas_Sensors_SGP30_Datasheet_EN.pdf#:~:text=first%2015s>
(ramp
up time?) before the algo wakes up, use an internal poll of 1second (which
the algo prefers
<https://cdn-learn.adafruit.com/assets/assets/000/050/058/original/Sensirion_Gas_Sensors_SGP30_Datasheet_EN.pdf#:~:text=The%20on>,
but
doesn't work well for low power). It suggests the baseline is useful for up
to a week
<https://files.seeedstudio.com/wiki/Grove-VOC_and_eCO2_Gas_Sensor-SGP30/res/Sensirion_Gas_Sensors_SGP30_Driver-Integration-Guide_HW_I2C.pdf#:~:text=one%20week>,
and initially only valid after 12hours
<https://files.seeedstudio.com/wiki/Grove-VOC_and_eCO2_Gas_Sensor-SGP30/res/Sensirion_Gas_Sensors_SGP30_Driver-Integration-Guide_HW_I2C.pdf#:~:text=12%20hours>.
So how would one expect that to play out?
…On Mon, 22 Sept 2025 at 16:11, Mikey Sklar ***@***.***> wrote:
*mikeysklar* left a comment (adafruit/Adafruit_Wippersnapper_Arduino#811)
<#811 (comment)>
@brentru <https://github.com/brentru> -
Welcome back. Thank you for the thorough review.
I'll hold off on the improved variable names and getting magic number
defined updates until you and @tyeth <https://github.com/tyeth> discuss
this.
—
Reply to this email directly, view it on GitHub
<#811 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABTBZ4ZTXXSFKBNRLT5UIFT3UAGSRAVCNFSM6AAAAACGLRAZG2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTGMJZGY2DOMJVGI>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
/*******************************************************************************/ | ||
/*! | ||
@brief Instanciates an I2C sensor. | ||
@brief Instanctiates an I2C sensor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no middle c
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeysklar Tyeth and I discussed this fix this morning. You are on the right track, but we'd like to remove the averaging code.
For the SGP30,
- Remove all the code related to averaging values within the driver.
fastTick
is OK but should only just read the driver. It should set a boolean flag ifIAQmeasure
succeeded.getEventX()
methods should first check the validity of the previous read (return value ofIAQmeasure()
) from fastTick, and fill the Event if the read was OK.
For the SGP40,
- Remove all the code related to averaging values within the driver.
- Add
uint16_t _rawValue
andint32_t _vocIdx
asprotected:
members fastTick
should just poll for raw value and voc inx, and save those into_rawValue
and_vocIdx
.- Within
getEventX()
, pack_rawValue
or_vocIdx
into an Event.
I added two comments below to reflect these changes. lmk if you need more info/have more questions, and thanks for taking a look at this. If you'd like Tyeth to take this over as well, let him know.
@brentru - Thank you for taking the time to make the above suggested changes. I'll start updating the PR with what you have requested and confirm functionality before asking for another review. @tyeth - The mixed cohorts of the various sensors do complicate things. My feeling is that if a sensors needs 30 minutes to get its bearings then it should be blocked from sleep. I'm looking at your BME68x. Maybe it makes sense to build on the per sensor JSON configs that WS v2 introduces and have fairly elaborate JSON config knobs. eg.
then rather than fighting for quality data publish the state of the data:
|
…emove averaging, cache single reads; minor doc/guard fixes
@mikeysklar Sorry, what 1Hz timer? |
I had a little millis counter with the 1000 magic number in the SGP30 / SGP40 code to poll the sensors at 1Hz. I ripped it up yesterday, but should probably put that back in to prevent constant polling.
|
Oh, yeah, that should go back in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeysklar I only have one change to request. Looks great.
@tyeth I've tagged you for secondary review since this touches the WipperSnapper_I2C
class.
thanks @tyeth - I like simple. I'm going to change up the SGP30 to look more like the SGP40 and just use the 1 Hz fastTick for sensor polling. No more consumed cache flag which is confusing. |
SGP30 arrived today so I'll try running both SGP30 and SGP40 with the latest artifacts and report back. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeysklar LGTM! Thank you for adding this PR
@tyeth Please pull this in when you are ready.
@mikeysklar released as v1.0.0-beta.117 |
Add SGP30 / SGP40 background sampling. Requires frequently 1s polling or returns 0 values.
Untested and only a proof of concept for feedback. This adds fastTick() polling to keep the SGPxx sensors producing usable data. Thanks @Timeline for the key insight of 1s polling being needed.
Difference observed between 15min polling versus 1s with the SGP40.
Address issue 732
Forum issues:
SGP30 issue with WipperSnapper
SGP40 Air Quality